﻿/*
	Copyright (c) 2013 Andrew Meyer

	Permission is hereby granted, free of charge, to any person obtaining a copy of this software
	and associated documentation files (the "Software"), to deal in the Software without
	restriction, including without limitation the right to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or
	substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
	BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Media.Media3D;

namespace Ascend
{
	/// <summary>
	/// An Ascend.TransformableObject that represents an object in the scene graph. This class
	/// cannot be inherited externally.
	/// </summary>
	public abstract class SceneNode : TransformableObject
	{
		#region Lifecycle
		internal SceneNode()
		{
			// Create children model group
			Data.Children.Add(new Model3DGroup());

			// Create child SceneNode collection. Each element will correspond to a model in the
			// children model group.
			Children = new SceneNodeCollection();

			// Listen to changes in children
			Children.CollectionChanged += Children_CollectionChanged;
		}
		#endregion

		#region Events
		/// <summary>
		/// Occurs when a mouse is clicked two or more times over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler MouseDoubleClick;

		/// <summary>
		/// Occurs when a mouse is clicked two or more times over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler PreviewMouseDoubleClick;

		/// <summary>
		/// Occurs when a mouse button is pressed over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler MouseDown;

		/// <summary>
		/// Occurs when a mouse button is pressed over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler PreviewMouseDown;

		/// <summary>
		/// Occurs when a mouse button is released over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler MouseUp;

		/// <summary>
		/// Occurs when a mouse button is released over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseButtonEventHandler PreviewMouseUp;

		/// <summary>
		/// Occurs when a mouse is moved while over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseEventHandler MouseMove;

		/// <summary>
		/// Occurs when a mouse is moved while over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseEventHandler PreviewMouseMove;

		/// <summary>
		/// Occurs when a mouse wheel is moved while over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseWheelEventHandler MouseWheel;

		/// <summary>
		/// Occurs when a mouse wheel is moved while over an Ascend.SceneNode.
		/// </summary>
		public event SceneNodeMouseWheelEventHandler PreviewMouseWheel;
		#endregion

		#region Internal Members
		internal Model3DCollection VisualChildren
		{
			get
			{
				return ((Model3DGroup)Data.Children[0]).Children;
			}
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// Gets or sets this Ascend.SceneNode's parent Ascend.SceneNode.
		/// </summary>
		public SceneNode Parent
		{
			get
			{
				return _transformParent as SceneNode;
			}

			set
			{
				if (Parent == value)
				{
					return;
				}

				if (value == null)
				{
					// Remove from curent parent (will set _transformParent = null)
					Parent.Children.Remove(this);
				}
				else
				{
					// Attach to new parent (will remove from existing parent and set
					// _transformParent = value)
					value.Children.Add(this);
				}
			}
		}

		/// <summary>
		/// Gets this Ascend.SceneNode's collection of child Ascend.SceneNode instances.
		/// </summary>
		public SceneNodeCollection Children
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets or sets the Ascend.Bone to which this Ascend.SceneNode is attached.
		/// </summary>
		public Bone BoneParent
		{
			get
			{
				return _transformParent as Bone;
			}

			set
			{
				if (BoneParent == value)
				{
					return;
				}

				if (value == null)
				{
					// Remove from curent bone parent (will set _transformParent = null)
					BoneParent.RigidAttachments.Remove(this);
				}
				else
				{
					// Attach to new bone parent (will remove from existing bone parent and set
					// _transformParent = value)
					value.RigidAttachments.Add(this);
				}
			}
		}

		/// <summary>
		/// Gets a read-only collection of Ascend.TransformableObject instances that are transformed
		/// relative to this Ascend.SceneNode.
		/// </summary>
		public override ReadOnlyCollection<TransformableObject> TransformChildren
		{
			get
			{
				return Children.ToList<TransformableObject>().AsReadOnly();
			}
		}
		#endregion

		#region Internal Methods
		internal void ClearTransformParent()
		{
			if (Parent != null)
			{
				Parent.Children.Remove(this);
			}
			else if (BoneParent != null)
			{
				BoneParent.RigidAttachments.Remove(this);
			}
		}
		#endregion

		#region Protected Methods
		protected virtual void CloneProperties(SceneNode clone)
		{
			clone.Name = Name;

			ClonePose(clone);
		}

		protected virtual void CloneChildren(SceneNode clone)
		{
			foreach (SceneNode child in Children)
			{
				clone.Children.Add((SceneNode)child.Clone());
			}
		}
		#endregion

		#region Event Invokers
		#region MouseDoubleClick
		internal static void RaiseMouseDoubleClickEvent(SceneNode target,
			SceneNodeMouseButtonEventArgs e)
		{
			target.RaiseMouseDoubleClickEvent(e);
		}

		internal void RaiseMouseDoubleClickEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (MouseDoubleClick != null)
			{
				foreach (Delegate del in MouseDoubleClick.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region PreviewMouseDoubleClick
		internal static void RaisePreviewMouseDoubleClickEvent(SceneNode target,
			SceneNodeMouseButtonEventArgs e)
		{
			target.RaisePreviewMouseDoubleClickEvent(e);
		}

		internal void RaisePreviewMouseDoubleClickEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (PreviewMouseDoubleClick != null)
			{
				foreach (Delegate del in PreviewMouseDoubleClick.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region MouseDown
		internal static void RaiseMouseDownEvent(SceneNode target, SceneNodeMouseButtonEventArgs e)
		{
			target.RaiseMouseDownEvent(e);
		}

		internal void RaiseMouseDownEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (MouseDown != null)
			{
				foreach (Delegate del in MouseDown.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region PreviewMouseDown
		internal static void RaisePreviewMouseDownEvent(SceneNode target,
			SceneNodeMouseButtonEventArgs e)
		{
			target.RaisePreviewMouseDownEvent(e);
		}

		internal void RaisePreviewMouseDownEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (PreviewMouseDown != null)
			{
				foreach (Delegate del in PreviewMouseDown.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region MouseUp
		internal static void RaiseMouseUpEvent(SceneNode target, SceneNodeMouseButtonEventArgs e)
		{
			target.RaiseMouseUpEvent(e);
		}

		internal void RaiseMouseUpEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (MouseUp != null)
			{
				foreach (Delegate del in MouseUp.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region PreviewMouseUp
		internal static void RaisePreviewMouseUpEvent(SceneNode target,
			SceneNodeMouseButtonEventArgs e)
		{
			target.RaisePreviewMouseUpEvent(e);
		}

		internal void RaisePreviewMouseUpEvent(SceneNodeMouseButtonEventArgs e)
		{
			if (PreviewMouseUp != null)
			{
				foreach (Delegate del in PreviewMouseUp.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region MouseMove
		internal static void RaiseMouseMoveEvent(SceneNode target, SceneNodeMouseEventArgs e)
		{
			target.RaiseMouseMoveEvent(e);
		}

		internal void RaiseMouseMoveEvent(SceneNodeMouseEventArgs e)
		{
			if (MouseMove != null)
			{
				foreach (Delegate del in MouseMove.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region PreviewMouseMove
		internal static void RaisePreviewMouseMoveEvent(SceneNode target, SceneNodeMouseEventArgs e)
		{
			target.RaisePreviewMouseMoveEvent(e);
		}

		internal void RaisePreviewMouseMoveEvent(SceneNodeMouseEventArgs e)
		{
			if (PreviewMouseMove != null)
			{
				foreach (Delegate del in PreviewMouseMove.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region MouseWheel
		internal static void RaiseMouseWheelEvent(SceneNode target, SceneNodeMouseWheelEventArgs e)
		{
			target.RaiseMouseWheelEvent(e);
		}

		internal void RaiseMouseWheelEvent(SceneNodeMouseWheelEventArgs e)
		{
			if (MouseWheel != null)
			{
				foreach (Delegate del in MouseWheel.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion

		#region PreviewMouseWheel
		internal static void RaisePreviewMouseWheelEvent(SceneNode target,
			SceneNodeMouseWheelEventArgs e)
		{
			target.RaisePreviewMouseWheelEvent(e);
		}

		internal void RaisePreviewMouseWheelEvent(SceneNodeMouseWheelEventArgs e)
		{
			if (PreviewMouseWheel != null)
			{
				foreach (Delegate del in PreviewMouseWheel.GetInvocationList())
				{
					if (e.Handled)
					{
						return;
					}

					del.Method.Invoke(del.Target, new object[] { this, e });
				}
			}
		}
		#endregion
		#endregion

		#region Event Handlers
		private void Children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					HandleCollectionActionAdd(sender, e);
					break;

				case NotifyCollectionChangedAction.Remove:
					HandleCollectionActionRemove(sender, e);
					break;
			}
		}

		private void HandleCollectionActionAdd(object sender, NotifyCollectionChangedEventArgs e)
		{
			foreach (SceneNode child in e.NewItems)
			{
				// Clear old parent
				child.ClearTransformParent();

				// Add data to model group
				VisualChildren.Add(child.Data);

				// Set new parent
				child._transformParent = this;
			}
		}

		private void HandleCollectionActionRemove(object sender, NotifyCollectionChangedEventArgs e)
		{
			foreach (SceneNode child in e.OldItems)
			{
				// Remove data from model group
				VisualChildren.Remove(child.Data);

				// Clear parent
				child._transformParent = null;
			}
		}
		#endregion
	}
}
